Skip to content

feat(devcontainer): un poste de developpement qui n'installe rien - #55

Merged
lostmind84 merged 15 commits into
mainfrom
feat/un-poste-de-dev-sans-rien-installer
Aug 11, 2026
Merged

feat(devcontainer): un poste de developpement qui n'installe rien#55
lostmind84 merged 15 commits into
mainfrom
feat/un-poste-de-dev-sans-rien-installer

Conversation

@lostmind84

Copy link
Copy Markdown
Owner

Un contributeur qui n'a que Docker peut désormais travailler sur ce dépôt : Go 1.26.5, Node 22, Python 3.13, gcc, PowerShell 7 et golangci-lint, tous aux versions que la CI épingle, sans rien installer d'autre sur son poste. Fonctionne sous Windows et sous Linux avec le même fichier.

La frontière, et elle est écrite

Six des sept vérifications de la CI se rejouent dans le conteneur : la passe -race, la passe sans cgo, les garde-fous, la compilation croisée, l'écran client et le handbook.

La septième ne le peut pas. Windows PowerShell 5.1 n'existe pas dans un conteneur Linux, et « estimer que les scripts sont corrects » est exactement la faute qui a livré la v0.1. Le job scripts de ci.yml reste le seul juge, à chaque pull request. La documentation le dit en une phrase plutôt que de le laisser deviner.

Le matériel ne pose pas de question : aucun test n'ouvre de port série — serial.Opener est une seam injectée, et hardware_test.go pose qu'une machine sans port série est « le cas de développement ORDINAIRE ». Un conteneur donne ce que donne un poste dont la balance est débranchée.

Ce que le conteneur rend à un développeur Windows

Deux bancs qu'un poste Windows n'exécute jamais :

  • la passe -race, qui se saute faute de gcc ;
  • TestADirectoryTheServiceCanReadButNotWriteIsRefused, qui saute sous Windows (« un répertoire Windows se ferme par une ACL et non par os.Chmod ») et sous root — d'où le remoteUser non root, sans quoi ce banc disparaîtrait en silence.

Le banc anti-dérive

deploy/devcontainer_test.go — onze bancs. Aucune version n'est décidée dans .devcontainer/ : Go vient de go.mod et de ci.yml, Node de ci.yml, Python de docs.yml, golangci-lint du Makefile par make -s golangci-version. Le banc les compare dans les deux sens et refuse qu'un numéro soit recopié. SUIVI.md rappelle que le seul compteur d'ADR a menti trois fois pour cette raison.

Il garde aussi ce qui disparaîtrait sans bruit : que l'image est bien construite depuis le Dockerfile inspecté, que le feature powershell est là, que chaque feature est épinglée par empreinte dans le fichier de verrou, et que les trois paquets apt — chacun attaché nommément au banc qu'il maintient en vie — n'ont pas été rognés.

Chaque banc a été vu rouge avant d'être commité. Une garantie qu'on n'a pas vue échouer n'est pas une garantie.

Deux correctifs que seule la construction réelle pouvait trouver

  • deploy/linux_test.go — le montage bind depuis Windows présente tout en 0777, et systemd-analyze refusait alors les unités sur leur mode de fichier, jamais sur leur contenu. Le banc vérifie désormais une copie à 0644. Il juge le contenu, seule chose que le checkout contrôle : install.sh pose install -m 0644 à l'installation, donc le mode du checkout n'atteint jamais la production.
  • post-create.sh — le même montage fait apparaître .git comme appartenant à root, git refuse le dépôt, go list perd l'estampille VCS, et make test meurt dans make boundary sur « 1 violation(s) — voir §5.2 », qui ne mentionne ni git ni les permissions. safe.directory referme la chaîne, et un banc refuse son retrait.

Mesuré, pas promis

Première construction de l'image 8 min 3 s, payée une fois
make test dedans, -race comprise 92 s, 70 paquets verts
make front-check vert, et internal/web/dist reste identique à l'octet
Depuis un hôte Linux uid 1000, écriture sans sudo, git status vide
Coût du bind Windows ×29 sur les métadonnées — 143 ms contre 5 ms pour parcourir 577 fichiers

Les caches Go et npm sont dans des volumes hors du montage : seule la lecture des sources paie cette taxe.

Ce qui n'est pas fait, et pourquoi

Pas de passthrough série — aucun test n'en a besoin. Pas de job CI qui construit l'image — le banc attrape la dérive de version pour quelques millisecondes, une construction coûterait cinq à six minutes par pull request. Ni le Makefile ni make.ps1 ne changent : le chemin sans conteneur reste la référence, et c'est lui que la CI exécute. Le devcontainer est une seconde porte.

Documentation

handbook/getting-started.md ouvre sur deux chemins. Le tableau des prérequis actuel est conservé intact comme chemin local. Le chemin conteneur mène par la CLI devcontainer, indépendante de l'éditeur — et c'est celui qui a réellement servi à construire et vérifier ce lot. Le compromis est dit franchement : un éditeur intégré ne demande que Docker, la CLI demande Docker et Node.

Conception et arbitrages : docs/superpowers/specs/2026-08-11-devcontainer-poste-sans-outillage-design.md.

…onception

La question était « WSL + devcontainer, sans outils sur le poste, Windows et
Linux : possible ? ». Oui, et ce document nomme la frontière plutôt que de
promettre l'équivalence.

Six des sept vérifications de la CI se rejouent dans le conteneur. La septième
— PowerShell 5.1 — ne le peut pas, et c'est écrit noir sur blanc : « estimer que
les scripts sont corrects » est la faute exacte qui a livré la v0.1.

Trois constats qui viennent du code et non d'une opinion :

  - Le matériel n'entre pas dans la question. serial.Opener est une seam
    injectée, et hardware_test.go pose qu'une machine sans port série est « le
    cas de développement ORDINAIRE ». Un conteneur donne ce que donne un poste
    dont la balance est débranchée.

  - Le conteneur RAJOUTE deux gardes à un développeur Windows : la passe -race,
    qui se saute faute de gcc, et TestADirectoryTheServiceCanReadButNotWriteIsRefused,
    qui saute sous Windows par nature — à condition que remoteUser ne soit pas
    root, sans quoi ce banc disparaît en silence.

  - Un devcontainer.json qui recopie Go 1.26.5, Node 22 et Python 3.13 en ferait
    un quatrième endroit où ces numéros vivent. SUIVI.md rappelle que le seul
    compteur d'ADR a menti trois fois pour cette raison : deploy/devcontainer_test.go
    les compare à go.mod, ci.yml et docs.yml dans les deux sens.

L'emplacement du dépôt sous Windows est tranché sur une mesure et non sur un
adjectif : 143 ms contre 5 ms pour parcourir 577 fichiers, soit x29. Le bind
Windows reste le chemin par défaut — c'est ce qu'un contributeur fera sans rien
lire — et le clone côté WSL est documenté comme accélérateur.

Ni le Makefile ni make.ps1 ne bougent : le devcontainer est une seconde porte.
Cinq tâches. Le banc anti-dérive est écrit AVANT les fichiers qu'il garde, sur
le modèle de tools/deps : sa première exécution est rouge, et ce sont les tâches
suivantes qui la font passer au vert. Un banc écrit après un fichier correct ne
dit jamais s'il rougirait le jour où le fichier cesse de l'être.

Le lecteur de JSONC est une tâche à lui seul parce qu'il porte le seul piège non
trivial du lot : un « // » à l'intérieur d'une chaîne n'est pas un commentaire,
et un lecteur naïf coupe « https://containers.dev » en deux pour livrer à
json.Unmarshal une chaîne non terminée.

Le banc vit dans deploy/ et non dans .devcontainer/ : l'outil Go ignore les
répertoires commençant par un point, et un test qui ne s'exécute jamais passe
pour un vert.

La tâche 4 ne se coche pas sur une sortie supposée. Elle casse le banc exprès —
1.26.5 en 1.26.6, vscode en root — pour le voir rougir, et vérifie le même
fichier depuis un hôte Linux, où un décalage d'UID ferait apparaître tout le
dépôt comme modifié.
devcontainer.json est du JSONC, et ce dépôt commente ses fichiers de
configuration. encoding/json refuse un commentaire : il faut donc les retirer
avant de décoder.

Le lecteur suit l'état des chaînes plutôt que de chercher deux caractères. Sans
cela, « https://containers.dev » serait coupé en deux et json.Unmarshal
répondrait « unterminated string » en nommant une ligne — on chercherait la
faute dans le mauvais fichier.
… les versions

Go 1.26.5 vit dans go.mod et ci.yml, Node 22 dans ci.yml, Python 3.13 dans
docs.yml, golangci-lint v2.12.2 dans le Makefile — chacun à un seul endroit, et
la CI lit le dernier plutôt que de le recopier. Un devcontainer.json qui
réécrirait ces numéros en ferait un quatrième endroit ; SUIVI.md rappelle que le
seul compteur d'ADR a menti trois fois pour cette raison.

Le banc compare dans les deux sens, et il exige aussi remoteUser non root :
TestADirectoryTheServiceCanReadButNotWriteIsRefused saute sous root ET sous
Windows, si bien qu'un conteneur root laisserait cette branche couverte par
rien tout en restant vert.

Rouge à ce commit : .devcontainer/ n'existe pas encore. C'est voulu — un banc
écrit après le fichier qu'il garde ne dit jamais s'il rougirait.
…se rejouent

Trois fichiers : l'image et ses trois paquets apt, la déclaration, et ce qui
s'installe après la construction.

Chaque paquet tient un banc et le dit : build-essential pour gcc, sans quoi la
passe -race se saute ; zip pour la cible release ; systemd pour systemd-analyze
seul. Le banc du commit précédent refuse qu'on allège cette liste en silence.

Les caches Go et npm sont dans des volumes, hors du dossier monté : sous
Windows, le bind coûte ×29 sur les métadonnées — 143 ms contre 5 ms pour
parcourir 577 fichiers. Sortis du bind, ils ne paient plus cette taxe.

post-create.sh lit la version de golangci-lint par « make -s golangci-version »
et l'installe hors module, comme ADR-039 l'exige et comme le fait déjà ci.yml.
…aient faux

Le chown de post-create.sh ne portait que sur go-build ; $HOME/.cache reste
un parent que Docker crée sous root, et c'est là que golangci-lint écrit.
Le chown couvre maintenant tout $HOME/.cache.

apt-get install manquait DEBIAN_FRONTEND=noninteractive : dbus ou
libpam-systemd, tirés par systemd, peuvent poser une question debconf, et
une invite dans docker build fige la construction au lieu d'échouer.
Placé dans le RUN, pas en ENV, pour ne pas fuiter dans le conteneur en
marche.

Quatre commentaires corrigés pour dire le vrai mécanisme : build-essential
fournit aussi make, dont post-create.sh dépend directement ; le banc ne
compare que trois versions et interdit l'écriture de la quatrième, il ne
les compare pas toutes ; npm ci est choisi pour son échec déterministe sur
un lock désynchronisé, pas parce qu'un « ^ » bougerait un fichier que ci
ne toucherait pas davantage ; et le shebang de post-create.sh n'est jamais
consulté puisque devcontainer.json l'invoque par bash, pas par exécution
directe — la règle LF reste vraie, seul le mécanisme de panne cité était
emprunté à install.sh.
Dans le devcontainer, le dépôt est monté par bind depuis l'hôte Windows,
et NTFS présente chaque fichier comme exécutable et inscriptible par
tous à Linux. systemd-analyze verify refusait donc les deux unités sur
leur MODE de fichier, jamais sur leur contenu, ce qui faisait rougir
TestTheUnitIsValidAccordingToSystemdItself sans qu'aucune unité ne
soit en cause.

Le banc copie désormais les deux unités dans un répertoire temporaire
avec le mode 0644, puis fait vérifier les copies par systemd-analyze.
Il juge ainsi le contenu des unités, indépendamment du système de
fichiers qui héberge le checkout.
`ghcr.io/devcontainers/features/go:1` est un tag MOBILE, et ci.yml dit déjà ce
que ça vaut : « un tag n'est pas une version, c'est un pointeur — et sur un
dépôt public, c'est le chemin d'attaque le moins coûteux qui existe contre une
chaîne de construction ». Le raisonnement qui épingle chaque action GitHub sur
un SHA de commit s'applique mot pour mot aux features du conteneur.

Ce fichier est produit par le CLI et non écrit à la main. Il ne déplace aucune
version : devcontainer.json continue de déclarer Go 1.26.5, Node 22 et Python
3.13, que deploy/devcontainer_test.go compare à go.mod, ci.yml et docs.yml.
Ce qu'il fige, c'est la révision des features elles-mêmes.
Sur un poste Windows, le workspace est monte tel quel et tout y appartient a
root alors que post-create.sh tourne sous vscode : git refuse le depot pour
« dubious ownership », et `go list` perd son horodatage VCS sur ce meme refus.
Le premier symptome vu ne nomme ni git ni les droits d'acces : c'est
« boundary: 1 violation(s) — voir docs/02-architecture.md §5.2 », qui fait
mourir `make test` dans `make boundary`. Touche tout contributeur passant par
le chemin par defaut (cloner sous Windows, rouvrir dans le conteneur).

Un banc dans deploy/devcontainer_test.go garde la ligne en place : verifie
rouge sans elle puis vert avec, avant ce commit.
…sion

« vscode » dans remoteUser est le nom d'un compte Unix livré par l'image de
base, sans rapport avec l'éditeur — une question réelle du propriétaire du
produit montre que ce n'était pas évident à la lecture.

Le bloc customizations.vscode.extensions n'avait aucune mention de sa portée :
un contributeur qui n'ouvre pas VS Code pouvait se demander si sa présence
rendait l'éditeur obligatoire. Elle ne le rend pas : la clé est ignorée par
tout ce qui n'est pas VS Code ou l'un de ses forks.
getting-started.md ouvre sur deux chemins au lieu d'un seul. Le tableau des
prérequis existant ne bouge pas : il devient le chemin local, qui reste la
référence.

Le chemin conteneur mène avec la CLI `devcontainer`, indépendante de tout
éditeur — c'est elle qui a servi à construire, lancer et tester cette
fonctionnalité de bout en bout, sans jamais ouvrir VS Code. Un éditeur qui la
porte (VS Code, Cursor, Windsurf, une JetBrains récente) n'a besoin que de
Docker ; la CLI demande en plus Node sur le poste. Les deux coûts mesurés
tiennent dans une note : 8 min 3 s pour la première image, 84 s pour
`make test` dedans, passe `-race` comprise.

Deux phrases devenaient fausses telles quelles — « pas de Docker » ici et dans
le README. Elles disaient qu'aucun outil n'est requis, ce qui reste vrai :
elles le disent maintenant en nommant la seconde porte.

Ce que le conteneur ne juge pas tient en une phrase et pas en un paragraphe :
les scripts d'installation sous Windows PowerShell 5.1, rendus par le job
« scripts » de la CI à chaque pull request.
…n seul

« Comptez cinq minutes » ouvrait la page avant toute bifurcation, alors que le
chemin conteneur — présenté en premier — coûte 8 min 3 s à la première
construction, mesurés. Le lecteur ne l'apprenait que quarante lignes plus bas.

La phrase annonce désormais les deux, et dit que la note salée n'est payée
qu'une fois.
Le banc anti-dérive avait trois angles morts. F1 : rien ne vérifiait que
devcontainer.json construit encore l'image depuis le Dockerfile qu'il
inspecte — un « image » préconstruit aurait laissé les sept tests verts
pendant que gcc, zip et systemd disparaissaient. F2 : la disparition du
feature powershell se traduit par un skip « ni pwsh ni powershell » qui
ressemble à une machine sans Windows, jamais à une perte. F3 : le fichier
de verrou devcontainer-lock.json n'était gardé par rien — le supprimer ou
y oublier un feature laissait tout vert.

F6 corrige aussi un banc existant : strings.Contains(dockerfile, "zip")
est satisfait par gzip ou bzip2. Un \b le ferme.

F9 étend le banc `sh -n` de deploy/shell_test.go à
.devcontainer/post-create.sh, jusque-là non analysé : une faute de
syntaxe n'y aurait été découverte qu'après une construction de huit
minutes.
…commentaire faux

F4 : source=openscale-node-modules était un nom fixe. Deux conteneurs
OpenScale ouverts à la fois — un arbre de revue et le clone principal —
partageaient un seul web/node_modules pour deux package-lock.json
potentiellement différents ; le `npm ci` de l'un vidait celui de l'autre,
sans qu'aucun message ne parle de montage. ${devcontainerId} isole désormais
ce volume par conteneur. GOMODCACHE et GOCACHE restent partagés : leur
contenu est adressé par version et l'outil Go verrouille ce qu'il lit, le
partage n'y est qu'un gain — la distinction est dans le commentaire.

F5 : le Dockerfile affirmait que post-create.sh appelle
`make -s golangci-version` en première commande réelle ; ce sont
`git config` puis `sudo chown` qui s'exécutent avant. L'argument de fond
(retirer ce paquet à gcc seul casserait le script, qui a besoin de make)
reste, reformulé sans l'affirmation fausse.

F7 : sous `set -e`, un `go install` en échec sortait le script avant le
`rm -rf "$install_dir"` de la ligne suivante, laissant le répertoire
temporaire. Un `trap ... EXIT` posé juste après le `mktemp -d` couvre les
deux chemins.

F8 : `npm ci --prefix web` s'aligne désormais sur `npm --prefix web ci`,
l'ordre que le Makefile emploie partout — un grep trouve maintenant tous
les appels npm avec la même forme.
…ier de verrou

§5.1 : l'image de base reste sur l'étiquette mobile ubuntu-24.04 quand
les features sont épinglées par empreinte — deux arbitrages du
propriétaire du produit, chacun sa raison, qui coexistaient jusque-là
sans qu'aucun des deux ne soit écrit.

§5.2 : devcontainer-lock.json n'était mentionné nulle part dans la
conception. Le paragraphe ajouté dit ce qu'il est (produit par la CLI,
jamais écrit à la main), pourquoi il est committé (dépôt public,
features/go:1 est une étiquette mobile) et comment le rafraîchir
(`devcontainer upgrade`) — sans quoi un fichier généré se fait un jour
supprimer comme un artefact.
@lostmind84
lostmind84 merged commit 42451d9 into main Aug 11, 2026
8 checks passed
@lostmind84
lostmind84 deleted the feat/un-poste-de-dev-sans-rien-installer branch August 11, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant